{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "QOLDAbVaU6Fa"
      },
      "source": [
        "# Contract Analysis Assistant\n",
        "\n",
        "![ClauseAI](../images/ClauseAI_logo.jpeg)\n",
        "\n",
        "## Overview\n",
        "\n",
        "Contract analysis requires precision and expertise across domains. Automating this process with AI agents enhances accuracy and saves time, providing actionable insights efficiently.\n",
        "\n",
        "Our solution is a multi-agent AI system designed to streamline contract analysis and deliver customized reports.\n",
        "\n",
        "---\n",
        "\n",
        "## Goal\n",
        "\n",
        "Develop an AI-powered system for contract analysis, generating insights and professional reports with minimal manual intervention.\n",
        "\n",
        "### Key Features:\n",
        "\n",
        "1. **Input Selection**:\n",
        "   - Users upload contracts and supporting documents or integrate external legal APIs.\n",
        "\n",
        "2. **AI Planning**:\n",
        "   - A team of AI analysts is generated, each specializing in a specific domain (e.g., compliance, finance, operations).\n",
        "   - `Human-in-the-loop` refines focus areas.\n",
        "\n",
        "3. **AI Research**:\n",
        "   - Analysts engage in multi-turn conversations with domain-specific AI experts.\n",
        "   - Discussions cover strengths, weaknesses, risks, and improvements in the contract.\n",
        "\n",
        "4. **Parallel Processing**:\n",
        "   - Researches and data extraction run simultaneously using `map-reduce` for speed and scalability.\n",
        "\n",
        "5. **Customizable Reports**:\n",
        "   - Insights are synthesized into professional reports tailored to user needs.\n",
        "\n",
        "---\n",
        "\n",
        "## Workflow\n",
        "\n",
        "1. **Input**: Upload contracts and related documents.\n",
        "2. **Analysis**: AI analysts and experts extract domain-specific insights.\n",
        "3. **Output**: Generate a comprehensive, actionable report.\n",
        "\n",
        "---\n",
        "\n",
        "## Benefits\n",
        "\n",
        "- **Efficiency**: Parallel processing reduces time.\n",
        "- **Accuracy**: Domain-specific expertise ensures precision.\n",
        "- **Customization**: Flexible, tailored reports.\n",
        "- **Scalability**: Process multiple contracts simultaneously.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "yOZjcLsCU6Fd"
      },
      "source": [
        "## Environment Setup\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 35,
      "metadata": {
        "id": "q7r-tbRIlE2G",
        "pycharm": {
          "name": "#%%\n"
        }
      },
      "outputs": [],
      "source": [
        "%%capture --no-stderr\n",
        "%pip install --quiet -U langgraph langchain-community langchain-openai docx pinecone[grpc] ipywidgets PyPDF2 python-docx\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 36,
      "metadata": {
        "id": "h0mAndAw9tWO",
        "pycharm": {
          "name": "#%%\n"
        }
      },
      "outputs": [],
      "source": [
        "from langchain_community.vectorstores import Pinecone\n",
        "from langchain_community.embeddings import OpenAIEmbeddings\n",
        "from typing import List, Dict, Optional\n",
        "from pydantic import BaseModel\n",
        "import json\n",
        "import os\n",
        "# Import the Pinecone library\n",
        "from pinecone.grpc import PineconeGRPC as Pinecone\n",
        "from pinecone import ServerlessSpec\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 37,
      "metadata": {
        "id": "NNB1mHFalHnr",
        "pycharm": {
          "name": "#%%\n"
        }
      },
      "outputs": [],
      "source": [
        "import os, getpass\n",
        "\n",
        "def _set_env(var: str):\n",
        "    if not os.environ.get(var):\n",
        "        os.environ[var] = getpass.getpass(f\"{var}: \")\n",
        "\n",
        "_set_env(\"OPENAI_API_KEY\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 38,
      "metadata": {
        "id": "QnDH5n_1-In_",
        "pycharm": {
          "name": "#%%\n"
        }
      },
      "outputs": [],
      "source": [
        "_set_env(\"PINECONE_API_KEY\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "F8ik4gj2U6Fh"
      },
      "source": [
        "## Clause Analysis Component\n",
        "\n",
        "This section of the code defines the `ClauseRetriever` class, which forms the core of the clause analysis functionality for the agent. It retrieves and indexes contract clauses based on contract type, category, and additional metadata such as jurisdiction and version.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 39,
      "metadata": {
        "id": "YcbUi2cF-wE5",
        "pycharm": {
          "name": "#%%\n"
        }
      },
      "outputs": [],
      "source": [
        "class ClauseMetadata(BaseModel):\n",
        "    jurisdiction: str\n",
        "    version: str\n",
        "    last_updated: str\n",
        "\n",
        "\n",
        "class ClauseRetriever:\n",
        "    def __init__(self, json_file_path: Optional[str] = None):\n",
        "        # Initialize Pinecone with GRPC client\n",
        "        self.pc = Pinecone(api_key=os.environ.get(\"PINECONE_API_KEY\"))\n",
        "\n",
        "        self.index_name = \"contract-clauses\"\n",
        "        self.embeddings = OpenAIEmbeddings()\n",
        "\n",
        "        # Get index instance\n",
        "        self.index = self.pc.Index(self.index_name)\n",
        "\n",
        "        # Initialize vector store\n",
        "        self.vectorstore = Pinecone(\n",
        "            index=self.index,\n",
        "            embedding=self.embeddings,\n",
        "            text_key=\"text\"\n",
        "        )\n",
        "\n",
        "        # Only load and index clauses if json_file_path is provided\n",
        "        if json_file_path:\n",
        "            self._load_clauses(json_file_path)\n",
        "\n",
        "    def _load_clauses(self, json_file_path: str):\n",
        "        \"\"\"Load and index clauses from JSON file\"\"\"\n",
        "        with open(json_file_path, 'r') as file:\n",
        "            self.contract_types = json.load(file)\n",
        "\n",
        "        # Process each contract type and its clauses\n",
        "        for contract_data in self.contract_types:\n",
        "            self._index_contract_clauses(contract_data)\n",
        "\n",
        "    def _index_contract_clauses(self, contract_data: Dict):\n",
        "        \"\"\"Index clauses for a specific contract type\"\"\"\n",
        "        contract_type = contract_data[\"contract_type\"]\n",
        "\n",
        "        vectors_to_upsert = []\n",
        "        for clause in contract_data[\"clauses\"]:\n",
        "            # Create the text to be embedded\n",
        "            clause_text = f\"\"\"\n",
        "            Contract Type: {contract_type}\n",
        "            Clause Title: {clause['clause_title']}\n",
        "\n",
        "            {clause['clause_text']}\n",
        "\n",
        "            \"\"\"\n",
        "\n",
        "            # Create metadata\n",
        "            metadata = {\n",
        "                \"contract_type\": contract_type,\n",
        "                \"clause_title\": clause[\"clause_title\"],\n",
        "                \"jurisdiction\": clause[\"metadata\"][\"jurisdiction\"],\n",
        "                \"version\": clause[\"metadata\"][\"version\"],\n",
        "                \"last_updated\": clause[\"metadata\"][\"last_updated\"],\n",
        "                \"text\": clause_text\n",
        "            }\n",
        "\n",
        "            # Get vector embedding\n",
        "            vector = self.embeddings.embed_query(clause_text)\n",
        "\n",
        "            # Add to upsert batch\n",
        "            vectors_to_upsert.append({\n",
        "                \"id\": f\"{contract_type}-{clause['clause_title']}\".lower().replace(\" \", \"-\"),\n",
        "                \"values\": vector,\n",
        "                \"metadata\": metadata\n",
        "            })\n",
        "\n",
        "            # Batch upsert in chunks of 100\n",
        "            if len(vectors_to_upsert) >= 100:\n",
        "                self.index.upsert(vectors=vectors_to_upsert)\n",
        "                vectors_to_upsert = []\n",
        "\n",
        "        # Upsert any remaining vectors\n",
        "        if vectors_to_upsert:\n",
        "            self.index.upsert(vectors=vectors_to_upsert)\n",
        "\n",
        "    def get_clauses_by_contract_type(self,\n",
        "                                   contract_type: str,\n",
        "                                   jurisdiction: Optional[str] = None,\n",
        "                                   k: int = 5) -> List[Dict]:\n",
        "        \"\"\"Retrieve relevant clauses based on contract type and optional filters\"\"\"\n",
        "        # Build filter dict\n",
        "        filter_dict = {\"contract_type\": contract_type}\n",
        "        if jurisdiction:\n",
        "            filter_dict[\"jurisdiction\"] = jurisdiction\n",
        "\n",
        "        # Create query vector\n",
        "        query_text = f\"Find clauses for {contract_type} contract\"\n",
        "        query_vector = self.embeddings.embed_query(query_text)\n",
        "\n",
        "        # Search for relevant clauses\n",
        "        results = self.index.query(\n",
        "            vector=query_vector,\n",
        "            top_k=k,\n",
        "            filter=filter_dict,\n",
        "            include_values=True,\n",
        "            include_metadata=True\n",
        "        )\n",
        "\n",
        "        # Format results\n",
        "        formatted_results = []\n",
        "        for match in results['matches']:\n",
        "            formatted_results.append({\n",
        "                \"clause_title\": match['metadata'][\"clause_title\"],\n",
        "                \"clause_text\": match['metadata'][\"text\"],\n",
        "                \"metadata\": match['metadata'],\n",
        "                \"relevance_score\": match['score']\n",
        "            })\n",
        "\n",
        "        return formatted_results\n",
        "\n",
        "    def search_clauses(self,\n",
        "                      query: str,\n",
        "                      contract_type: Optional[str] = None,\n",
        "                      jurisdiction: Optional[str] = None,\n",
        "                      k: int = 5) -> List[Dict]:\n",
        "        \"\"\"Search for clauses based on semantic similarity\"\"\"\n",
        "        # Build filter dict\n",
        "        filter_dict = {}\n",
        "        if contract_type:\n",
        "            filter_dict[\"contract_type\"] = contract_type\n",
        "        if jurisdiction:\n",
        "            filter_dict[\"jurisdiction\"] = jurisdiction\n",
        "\n",
        "        # Create query vector\n",
        "        query_vector = self.embeddings.embed_query(query)\n",
        "\n",
        "        # Perform search\n",
        "        results = self.index.query(\n",
        "            vector=query_vector,\n",
        "            top_k=k,\n",
        "            filter=filter_dict if filter_dict else None,\n",
        "            include_values=True,\n",
        "            include_metadata=True\n",
        "        )\n",
        "\n",
        "        # Format results\n",
        "        formatted_results = []\n",
        "        for match in results['matches']:\n",
        "            formatted_results.append({\n",
        "                \"clause_title\": match['metadata'][\"clause_title\"],\n",
        "                \"clause_text\": match['metadata'][\"text\"],\n",
        "                \"metadata\": match['metadata'],\n",
        "                \"relevance_score\": match['score']\n",
        "            })\n",
        "\n",
        "        return formatted_results"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "R6vxx8FNU6Fi"
      },
      "source": [
        "## Load clauses.json to pinecone, it should be in the data folder\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 40,
      "metadata": {
        "id": "SgxgLxiK-zTt",
        "pycharm": {
          "name": "#%%\n"
        }
      },
      "outputs": [],
      "source": [
        "clause_retriever = ClauseRetriever(\"../data/clauses.json\")\n",
        "employment_clauses = clause_retriever.get_clauses_by_contract_type(\n",
        "        contract_type=\"Employment Contract\",\n",
        "    )"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 51,
      "metadata": {
        "id": "M6AdLnDHo18Y",
        "pycharm": {
          "name": "#%%\n"
        }
      },
      "outputs": [],
      "source": [
        "from typing import Annotated, List, Optional\n",
        "from typing_extensions import TypedDict\n",
        "from pydantic import BaseModel, Field\n",
        "from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n",
        "from langchain_openai import ChatOpenAI\n",
        "from langgraph.graph import END, MessagesState, START, StateGraph\n",
        "from langgraph.checkpoint.memory import MemorySaver\n",
        "from langgraph.store.memory import InMemoryStore\n",
        "import operator\n",
        "import os\n",
        "import getpass\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 52,
      "metadata": {
        "id": "-hdhyS86lTOY",
        "pycharm": {
          "name": "#%%\n"
        }
      },
      "outputs": [],
      "source": [
        "from langchain_openai import ChatOpenAI\n",
        "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "collapsed": false,
        "pycharm": {
          "name": "#%% md\n"
        },
        "id": "CL_my5ROU6Fk"
      },
      "source": [
        "We'll use [LangSmith](https://docs.smith.langchain.com/) for [tracing](https://docs.smith.langchain.com/concepts/tracing)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 53,
      "metadata": {
        "pycharm": {
          "name": "#%%\n"
        },
        "id": "0_MA1RyUU6Fk"
      },
      "outputs": [],
      "source": [
        "_set_env(\"LANGCHAIN_API_KEY\")\n",
        "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
        "os.environ[\"LANGCHAIN_PROJECT\"] = \"langchain-academy\""
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "collapsed": false,
        "pycharm": {
          "name": "#%% md\n"
        },
        "id": "dxo9CQgCU6Fk"
      },
      "source": [
        "## Contract Review State and Supporting Models\n",
        "\n",
        "This section defines the core data structures used for managing the state of a contract review process, detailing the contract type, review steps, modifications, and analysis.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 54,
      "metadata": {
        "pycharm": {
          "name": "#%%\n"
        },
        "id": "RZKUalaOU6Fk"
      },
      "outputs": [],
      "source": [
        "class ContractInfo(BaseModel):\n",
        "    contract_type: str = Field(description=\"Type of the contract\")\n",
        "    industry: Optional[str] = Field(description=\"Industry if identifiable\")\n",
        "\n",
        "class ReviewPlan(BaseModel):\n",
        "    steps: List[str] = Field(description=\"Detailed steps for contract review\")\n",
        "\n",
        "class Modification(BaseModel):\n",
        "    original_text: str = Field(description=\"Original contract text\")\n",
        "    suggested_text: str = Field(description=\"Suggested modification\")\n",
        "    reason: str = Field(description=\"Reason for modification\")\n",
        "\n",
        "class ContractReviewState(TypedDict):\n",
        "    contract_text: str\n",
        "    primary_objective: str\n",
        "    specific_focus: Optional[str]\n",
        "    contract_info: ContractInfo\n",
        "    review_plan: ReviewPlan\n",
        "    current_step: int\n",
        "    modifications: Annotated[List[Modification], operator.add]\n",
        "    clause_modifications: Annotated[List[Modification], operator.add]\n",
        "    sections: Annotated[List[str], operator.add]\n",
        "    clause_analysis: Annotated[List[str], operator.add]\n",
        "    clauses: Annotated[List[str], operator.add]\n",
        "    final_report: str\n",
        "\n",
        "class StepAnalysis(BaseModel):\n",
        "    modifications: List[Modification] = Field(default_factory=list, description=\"List of suggested modifications\")\n",
        "    analysis: str = Field(description=\"Analysis from this role's perspective\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "collapsed": false,
        "pycharm": {
          "name": "#%% md\n"
        },
        "id": "6DuGK1UPU6Fl"
      },
      "source": [
        "## Contract Review Nodes Overview\n",
        "\n",
        "This section outlines the key nodes in the workflow that perform tasks such as classifying contracts, retrieving clauses, generating review plans, and creating final reports. Each node plays a crucial role in the AI-driven contract analysis process.\n",
        "\n",
        "---\n",
        "\n",
        "### **1. Classify Contract**\n",
        "This node analyzes the contract to classify:\n",
        "- **Contract Type**: Identifies the type of contract (e.g., NDA, Employment Agreement, License Agreement).\n",
        "- **Industry**: Determines the relevant industry based on the context, if identifiable.\n",
        "\n",
        "This classification provides a foundation for tailoring the subsequent review and analysis steps.\n",
        "\n",
        "---\n",
        "\n",
        "### **2. Retrieve Clauses**\n",
        "This node retrieves relevant clauses by:\n",
        "- Extracting **General Clauses** that apply universally.\n",
        "- Retrieving **Specific Clauses** related to the identified contract type.\n",
        "- Filtering clauses for **relevancy** using semantic analysis.\n",
        "\n",
        "The output includes formatted clauses for analysis and integration into the review workflow.\n",
        "\n",
        "---\n",
        "\n",
        "### **3. Execute Clause Analysis**\n",
        "This node evaluates individual clauses for clarity and completeness:\n",
        "- Checks if clauses are clearly represented and unambiguous.\n",
        "- Identifies missing elements or unclear language.\n",
        "- Suggests modifications if necessary.\n",
        "\n",
        "This ensures that all critical clauses are accurate and appropriately integrated.\n",
        "\n",
        "---\n",
        "\n",
        "### **4. Create Review Plan**\n",
        "Generates a detailed review plan based on the contract type and specific focus areas. Each step in the plan represents a legal role or perspective, such as:\n",
        "- Employment Law Specialist.\n",
        "- Compliance Officer.\n",
        "- Intellectual Property Counsel.\n",
        "- Financial Terms Analyst.\n",
        "\n",
        "The review plan ensures a comprehensive and role-based approach to contract evaluation.\n",
        "\n",
        "---\n",
        "\n",
        "### **5. Execute Step**\n",
        "Executes a specific step from the review plan, focusing on:\n",
        "- Analyzing the contract from the perspective of a particular role.\n",
        "- Identifying and suggesting modifications with clear justifications.\n",
        "- Providing a summary of findings for that step.\n",
        "\n",
        "This iterative process progresses through all the steps defined in the review plan.\n",
        "\n",
        "---\n",
        "\n",
        "### **6. Generate Final Report**\n",
        "Compiles a comprehensive report summarizing:\n",
        "- The contract's classification, objectives, and specific focus areas.\n",
        "- Key findings from the review steps and clause analysis.\n",
        "- Highlights of suggested modifications with legal justifications.\n",
        "- Compliance and risk assessment insights.\n",
        "\n",
        "The final report serves as a structured output, ready for legal review and decision-making.\n",
        "\n",
        "---\n",
        "\n",
        "### Purpose of Nodes\n",
        "These nodes collectively form an AI-powered workflow for contract analysis, ensuring:\n",
        "- Accurate classification of contracts.\n",
        "- Context-aware clause retrieval and review.\n",
        "- Comprehensive role-based evaluation.\n",
        "- Clear and actionable outputs for legal professionals.\n"
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "def classify_contract(state: ContractReviewState):\n",
        "    \"\"\"Node to classify the contract type and industry\"\"\"\n",
        "\n",
        "    system_prompt = \"\"\"Analyze the provided contract and determine:\n",
        "    1. The type of contract (e.g., Employment, NDA, License Agreement)\n",
        "    2. The industry it belongs to (if clear from the context).\"\"\"\n",
        "\n",
        "    messages = [\n",
        "        SystemMessage(content=system_prompt),\n",
        "        HumanMessage(content=f\"Contract text:\\n{state['contract_text']}\")\n",
        "    ]\n",
        "\n",
        "    contract_info = llm.with_structured_output(ContractInfo).invoke(messages)\n",
        "    return {\"contract_info\": contract_info}\n",
        "\n",
        "def retrieve_clauses(state: ContractReviewState):\n",
        "    \"\"\"Node to retrieve clauses based on contract type and filter for relevancy\"\"\"\n",
        "    contract_type = state['contract_info'].contract_type\n",
        "\n",
        "    try:\n",
        "        # First get ALL General Clauses (no limit)\n",
        "        general_clauses = clause_retriever.get_clauses_by_contract_type(\n",
        "            contract_type=\"General Clauses\",\n",
        "            k=10  # Using a high number to effectively get all clauses\n",
        "        )\n",
        "\n",
        "        # Then get specific contract clauses\n",
        "        specific_clauses = clause_retriever.get_clauses_by_contract_type(\n",
        "            contract_type=contract_type,\n",
        "            k=10  # Get more clauses initially since we'll filter them\n",
        "        )\n",
        "\n",
        "        # Filter specific clauses for relevancy using LLM\n",
        "        system_prompt = f\"\"\"You are a legal clause relevancy analyzer.\n",
        "        For each clause, determine if it is relevant for a {contract_type}.\n",
        "        Respond with either \"RELEVANT\" or \"NOT RELEVANT\".\n",
        "        Base your decision on how essential and appropriate the clause is for this type of contract.\"\"\"\n",
        "\n",
        "        filtered_specific_clauses = []\n",
        "        for clause in specific_clauses:\n",
        "            messages = [\n",
        "                SystemMessage(content=system_prompt),\n",
        "                HumanMessage(content=f\"Clause Title: {clause['clause_title']}\\n\\nClause Text: {clause['clause_text']}\")\n",
        "            ]\n",
        "\n",
        "            response = llm.invoke(messages).content.strip().upper()\n",
        "            if response == \"RELEVANT\":\n",
        "                filtered_specific_clauses.append(clause)\n",
        "\n",
        "        # Format clauses for inclusion in the state\n",
        "        formatted_clauses = []\n",
        "\n",
        "        # Add ALL General Clauses first\n",
        "        for clause in general_clauses:\n",
        "            formatted_clause = f\"\"\"### {clause['clause_title']}\n",
        "\n",
        "            {clause['clause_text']}\n",
        "            \"\"\"\n",
        "            formatted_clauses.append(formatted_clause)\n",
        "\n",
        "        # Then add filtered specific contract clauses\n",
        "        for clause in filtered_specific_clauses:\n",
        "            formatted_clause = f\"\"\"### {clause['clause_title']}\n",
        "\n",
        "            {clause['clause_text']}\n",
        "            \"\"\"\n",
        "            formatted_clauses.append(formatted_clause)\n",
        "\n",
        "        return {\"clauses\": formatted_clauses,\n",
        "                 \"current_step\": 0}  # Return to clauses instead of sections\n",
        "\n",
        "    except Exception as e:\n",
        "        error_message = f\"Error retrieving clauses: {str(e)}\"\n",
        "        return {\"clauses\": [error_message]}\n",
        "\n",
        "\n",
        "def execute_step_clause(state: ContractReviewState):\n",
        "    \"\"\"Node to verify if each clause is clearly represented in the contract\"\"\"\n",
        "\n",
        "    clause = state['clauses'][0]\n",
        "\n",
        "    system_prompt = f\"\"\"You are a Legal Clause Clarity Analyst.\n",
        "    Review the contract and determine if the following clause is clearly represented:\n",
        "\n",
        "    {clause}\n",
        "\n",
        "    Guidelines for your review:\n",
        "    1. Check if the clause's key elements are present in the contract\n",
        "    2. Verify if the language is clear and unambiguous\n",
        "    3. Suggest modifications only if the clause is missing or unclear\"\"\"\n",
        "\n",
        "    messages = [\n",
        "        SystemMessage(content=system_prompt),\n",
        "        HumanMessage(content=state['contract_text'])\n",
        "    ]\n",
        "\n",
        "    step_result = llm.with_structured_output(StepAnalysis).invoke(messages)\n",
        "    clause_summary = f\"### Clause Analysis\\n{step_result.analysis}\"\n",
        "\n",
        "    return_dict = {\n",
        "        \"clause_analysis\": [clause_summary]\n",
        "    }\n",
        "\n",
        "    if step_result.modifications:\n",
        "        return_dict[\"clause_modifications\"] = step_result.modifications\n",
        "\n",
        "    return return_dict\n",
        "\n",
        "\n",
        "def create_review_plan(state: ContractReviewState):\n",
        "    \"\"\"Node to create a detailed review plan based on different legal roles/perspectives\"\"\"\n",
        "\n",
        "    system_prompt = \"\"\"You are a legal contract review planner.\n",
        "    Create a review plan where each step represents a different legal role/perspective for reviewing the contract.\n",
        "\n",
        "    Context:\n",
        "    - Contract Type: {contract_type}\n",
        "    - Industry: {industry}\n",
        "    - Primary Objective: {objective}\n",
        "    - Specific Focus: {focus}\n",
        "\n",
        "    Each step should be a specific role perspective, such as:\n",
        "    - Employment Law Specialist Review\n",
        "    - Intellectual Property Counsel Review\n",
        "    - Compliance Officer Review\n",
        "    - Financial Terms Specialist Review\n",
        "    - Risk Management Review\n",
        "    - Data Privacy Officer Review\n",
        "\n",
        "    Do not include generic steps or specific clause analysis - that will happen during execution.\"\"\".format(\n",
        "        contract_type=state['contract_info'].contract_type,\n",
        "        industry=state['contract_info'].industry or \"Not specified\",\n",
        "        objective=state['primary_objective'],\n",
        "        focus=state['specific_focus'] or \"Not specified\"\n",
        "    )\n",
        "\n",
        "    messages = [\n",
        "        SystemMessage(content=system_prompt),\n",
        "        HumanMessage(content=f\"Contract text:\\n{state['contract_text']}\\n\\nGenerate a role-based review plan.\")\n",
        "    ]\n",
        "\n",
        "    review_plan = llm.with_structured_output(ReviewPlan).invoke(messages)\n",
        "    return {\n",
        "        \"review_plan\": review_plan,\n",
        "        \"current_step\": 0\n",
        "    }\n",
        "\n",
        "def execute_step(state: ContractReviewState):\n",
        "    \"\"\"Node to execute each step of the review plan with specific analysis\"\"\"\n",
        "\n",
        "    role = state['review_plan'][0]\n",
        "\n",
        "    system_prompt = f\"\"\"You are a {role}.\n",
        "    Review the contract from your professional perspective.\n",
        "\n",
        "    Guidelines for your review:\n",
        "    1. Identify specific sections that fall under your expertise\n",
        "    2. Analyze those sections in detail\n",
        "    3. Suggest concrete modifications where necessary\n",
        "\n",
        "    Your response should include:\n",
        "    1. analysis: A detailed explanation of your review findings\n",
        "    2. modifications: A list of suggested changes, each containing:\n",
        "       - original_text: The exact text to be modified\n",
        "       - suggested_text: Your proposed replacement\n",
        "       - reason: Clear reasoning for the change based on your role\n",
        "\n",
        "    You may suggest multiple modifications or none if appropriate.\"\"\"\n",
        "\n",
        "    messages = [\n",
        "        SystemMessage(content=system_prompt),\n",
        "        HumanMessage(content=state['contract_text'])\n",
        "    ]\n",
        "\n",
        "    step_result = llm.with_structured_output(StepAnalysis).invoke(messages)\n",
        "    section_summary = f\"### {role}\\n{step_result.analysis}\"\n",
        "\n",
        "    return {\n",
        "        \"modifications\": step_result.modifications,\n",
        "        \"sections\": [section_summary]\n",
        "    }\n",
        "\n",
        "def generate_final_report(state: ContractReviewState):\n",
        "    \"\"\"Node to generate the final report and summary.\"\"\"\n",
        "    # Extract relevant data from the state\n",
        "    contract_text = state[\"contract_text\"]\n",
        "    primary_objective = state[\"primary_objective\"]\n",
        "    specific_focus = state.get(\"specific_focus\", \"Not specified\")\n",
        "    contract_type = state[\"contract_info\"].contract_type\n",
        "    industry = state[\"contract_info\"].industry or \"Not specified\"\n",
        "    review_plan = state[\"review_plan\"]\n",
        "    clause_modifications = state[\"clause_modifications\"]\n",
        "    planner_modifications = state[\"modifications\"]\n",
        "    sections = state[\"sections\"]\n",
        "    clause_analysis = state[\"clause_analysis\"]\n",
        "    clauses = state[\"clauses\"]\n",
        "\n",
        "    # Combine all modifications for LLM processing\n",
        "    all_modifications = clause_modifications + planner_modifications\n",
        "\n",
        "    # Prepare the system prompt\n",
        "    system_prompt = (\n",
        "        \"You're a modifications reviewer, you get a list of modifications.\\n\"\n",
        "        \"class Modification(BaseModel):\\n\"\n",
        "        \"  original_text: str = Field(description='Original contract text')\\n\"\n",
        "        \"  suggested_text: str = Field(description='Suggested modification')\\n\"\n",
        "        \"  reason: str = Field(description='Reason for modification')\\n\\n\"\n",
        "        \"Please summarize them, mostly focusing on the reason and explain it from a legal expert's point of view.\\n\\n\"\n",
        "        \"The modifications:\\n\"\n",
        "        f\"{all_modifications}\\n\"\n",
        "    )\n",
        "\n",
        "    # Generate the messages\n",
        "    messages = [\n",
        "        SystemMessage(content=system_prompt),\n",
        "        HumanMessage(content=\"Please summarize the modifications\")\n",
        "    ]\n",
        "\n",
        "    # Invoke the LLM\n",
        "    try:\n",
        "        review_plan = llm.invoke(messages)\n",
        "        modification_summary = review_plan.content\n",
        "    except Exception as e:\n",
        "        modification_summary = f\"Error generating summary: {str(e)}\"\n",
        "\n",
        "    # Generate the report\n",
        "    report = \"\\n\".join([\n",
        "        \"===============================================\",\n",
        "        \"                  Contract Review Report       \",\n",
        "        \"===============================================\",\n",
        "        \"\",\n",
        "        \"Contract Overview\",\n",
        "        \"-----------------\",\n",
        "        f\"Primary Objective: {primary_objective}\",\n",
        "        f\"Specific Focus: {specific_focus}\",\n",
        "        \"\",\n",
        "        f\"Contract Type: {contract_type}\",\n",
        "        f\"Industry: {industry}\",\n",
        "        \"\",\n",
        "        \"Sections and Clauses Analyzed:\",\n",
        "        \"------------------------------\",\n",
        "        f\"Total Sections Reviewed: {len(sections)}\",\n",
        "        f\"Total Clauses Analyzed: {len(clauses)}\",\n",
        "        \"\",\n",
        "        \"Key Findings and Analysis:\",\n",
        "        \"--------------------------\",\n",
        "        \"\\n\".join(f\"- {analysis}\" for analysis in clause_analysis),\n",
        "        \"\",\n",
        "        \"Highlights of Suggested Modifications:\",\n",
        "        \"--------------------------------------\",\n",
        "        modification_summary,\n",
        "        \"\",\n",
        "        \"Compliance and Risk Assessment:\",\n",
        "        \"-------------------------------\",\n",
        "        \"- The contract has been reviewed for compliance with relevant laws and regulations.\",\n",
        "        \"- Potential risks and mitigation strategies have been identified.\",\n",
        "        \"- Tailored suggestions have been provided to enhance the contract’s effectiveness.\",\n",
        "        \"\",\n",
        "        \"Final Notes:\",\n",
        "        \"------------\",\n",
        "        \"Please ensure all suggested modifications are incorporated and reviewed by a legal expert before finalizing the contract.\",\n",
        "        \"\",\n",
        "        \"===============================================\",\n",
        "        \"                  End of Report               \",\n",
        "        \"===============================================\",\n",
        "    ])\n",
        "\n",
        "    return {\"final_report\": report}\n",
        "\n"
      ],
      "metadata": {
        "id": "O5sPBUAZpgPE"
      },
      "execution_count": 90,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "## Parallel work\n",
        "\n",
        "#### check the clauses and the steps of the plan in parallel using map-reduce\n",
        "\n"
      ],
      "metadata": {
        "id": "cdRzxahfu2YR"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from langgraph.constants import Send\n",
        "\n",
        "\n",
        "def continue_to_clauses_check_execute(state: ContractReviewState):\n",
        "    return [Send(\"execute_step_clause\", {\"contract_text\": state[\"contract_text\"], \"clauses\": c}) for c in state[\"clauses\"]]\n",
        "\n",
        "\n",
        "def continue_to_plan_check_execute(state: ContractReviewState):\n",
        "    return [Send(\"execute_step\", {\"contract_text\": state[\"contract_text\"], \"review_plan\": step}) for step in state[\"review_plan\"].steps]\n",
        "\n"
      ],
      "metadata": {
        "id": "ods0zbj_o7kP"
      },
      "execution_count": 99,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "collapsed": false,
        "pycharm": {
          "name": "#%% md\n"
        },
        "id": "qL_KNzj7U6Fm"
      },
      "source": [
        "## The graph"
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "# Create the graph\n",
        "builder = StateGraph(ContractReviewState)\n",
        "\n",
        "# Add nodes\n",
        "builder.add_node(\"classify_contract\", classify_contract)\n",
        "builder.add_node(\"retrieve_clauses\", retrieve_clauses)\n",
        "builder.add_node(\"execute_step_clause\", execute_step_clause)\n",
        "builder.add_node(\"create_review_plan\", create_review_plan)\n",
        "builder.add_node(\"execute_step\", execute_step)\n",
        "builder.add_node(\"generate_final_report\", generate_final_report)\n",
        "\n",
        "# Add edges\n",
        "builder.add_edge(START, \"classify_contract\")\n",
        "builder.add_edge(\"classify_contract\", \"retrieve_clauses\")\n",
        "\n",
        "builder.add_conditional_edges(\"retrieve_clauses\", continue_to_clauses_check_execute, [\"execute_step_clause\"])\n",
        "builder.add_edge(\"execute_step_clause\", \"create_review_plan\")\n",
        "\n",
        "builder.add_conditional_edges(\"create_review_plan\", continue_to_plan_check_execute, [\"execute_step\"])\n",
        "builder.add_edge(\"execute_step\", \"generate_final_report\")\n",
        "\n",
        "builder.add_edge(\"generate_final_report\", END)\n",
        "\n",
        "# Compile the graph\n",
        "\n",
        "# Create the checkpointer\n",
        "checkpointer = MemorySaver()\n",
        "\n",
        "# Create memory store\n",
        "in_memory_store = InMemoryStore()\n",
        "\n",
        "# Compile graph with both checkpointer and store\n",
        "graph = builder.compile(\n",
        "    checkpointer=checkpointer,\n",
        "    store=in_memory_store\n",
        ")"
      ],
      "metadata": {
        "id": "QMa5D8PZqx9O"
      },
      "execution_count": 100,
      "outputs": []
    },
    {
      "cell_type": "code",
      "execution_count": 101,
      "metadata": {
        "pycharm": {
          "name": "#%%\n"
        },
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 748
        },
        "id": "A4M6AyWCU6Fn",
        "outputId": "026303c4-2cfd-494c-f7f8-cd925921ae17"
      },
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "image/png": "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\n",
            "text/plain": [
              "<IPython.core.display.Image object>"
            ]
          },
          "metadata": {}
        }
      ],
      "source": [
        "from IPython.display import Image, display\n",
        "\n",
        "display(Image(graph.get_graph(xray=True).draw_mermaid_png()))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "collapsed": false,
        "pycharm": {
          "name": "#%% md\n"
        },
        "id": "WXPMbI-ZU6Fn"
      },
      "source": [
        "## An Example Usage\n",
        "\n",
        "### upload a contract file and describe your Primary Objective and Specific Focus (optional)"
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "import ipywidgets as widgets\n",
        "from IPython.display import display\n",
        "import PyPDF2\n",
        "import docx\n",
        "\n",
        "# Function to extract text from a PDF\n",
        "def extract_text_from_pdf(file_path):\n",
        "    with open(file_path, 'rb') as file:\n",
        "        reader = PyPDF2.PdfReader(file)\n",
        "        text = \"\"\n",
        "        for page in reader.pages:\n",
        "            text += page.extract_text()\n",
        "    return text\n",
        "\n",
        "# Function to extract text from a DOCX file\n",
        "def extract_text_from_docx(file_path):\n",
        "    doc = docx.Document(file_path)\n",
        "    text = \"\\n\".join([paragraph.text for paragraph in doc.paragraphs])\n",
        "    return text\n",
        "\n",
        "# Variables to store file content and user inputs\n",
        "uploaded_file_path = None\n",
        "contract = \"\"\n",
        "primary_objective = \"\"\n",
        "specific_focus = \"\"\n",
        "\n",
        "# Callback function to handle file upload\n",
        "def handle_file_upload(change):\n",
        "    global uploaded_file_path\n",
        "    uploaded_file = change['new']\n",
        "    for file_name, file_info in uploaded_file.items():\n",
        "        uploaded_file_path = file_name\n",
        "        with open(file_name, 'wb') as f:\n",
        "            f.write(file_info['content'])\n",
        "        print(f\"File {file_name} uploaded successfully!\")\n",
        "\n",
        "# File upload widget\n",
        "upload_widget = widgets.FileUpload(accept='.pdf,.docx', multiple=False)\n",
        "upload_widget.observe(handle_file_upload, names='value')\n",
        "\n",
        "# Text inputs for primary_objective and specific_focus\n",
        "primary_objective_input = widgets.Text(\n",
        "    description=\"Primary Objective:\",\n",
        "    placeholder=\"Enter the primary objective\",\n",
        ")\n",
        "\n",
        "specific_focus_input = widgets.Text(\n",
        "    description=\"Specific Focus:\",\n",
        "    placeholder=\"Enter the specific focus\",\n",
        ")\n",
        "\n",
        "# Button to save inputs and extract file content\n",
        "def save_inputs_and_extract_file_content(_):\n",
        "    global contract, primary_objective, specific_focus\n",
        "\n",
        "    # Check if a file has been uploaded\n",
        "    if not uploaded_file_path:\n",
        "        print(\"Please upload a file before saving inputs!\")\n",
        "        return\n",
        "\n",
        "    # Determine file type and extract text\n",
        "    if uploaded_file_path.endswith('.pdf'):\n",
        "        contract = extract_text_from_pdf(uploaded_file_path)\n",
        "    elif uploaded_file_path.endswith('.docx'):\n",
        "        contract = extract_text_from_docx(uploaded_file_path)\n",
        "    else:\n",
        "        print(\"Unsupported file format. Please upload a PDF or DOCX file.\")\n",
        "        return\n",
        "\n",
        "    # Save user inputs\n",
        "    primary_objective = primary_objective_input.value\n",
        "    specific_focus = specific_focus_input.value\n",
        "\n",
        "    # Display the results\n",
        "    print(\"\\n--- Results ---\")\n",
        "    print(f\"Extracted Contract Text (first 500 chars):\\n{contract[:500]}...\")  # Showing a snippet for clarity\n",
        "    print(f\"Primary Objective: {primary_objective}\")\n",
        "    print(f\"Specific Focus: {specific_focus}\")\n",
        "\n",
        "save_button = widgets.Button(description=\"Save & Extract\")\n",
        "save_button.on_click(save_inputs_and_extract_file_content)\n",
        "\n",
        "# Display widgets\n",
        "display(upload_widget)\n",
        "display(primary_objective_input)\n",
        "display(specific_focus_input)\n",
        "display(save_button)\n"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 353,
          "referenced_widgets": [
            "c1ec52b61f0347fc9bd4a922ad702c21",
            "4b19a34d51484f3cbaacb77b2d04e210",
            "d89a6ec0ed8546079a964b74fd58ff59",
            "ceaf2d0ed69e48a4950358def791c71a",
            "b5d85e4e6cb04eb59d71861d5e4dfe83",
            "0ae65eea03cb40bdae641fe9cd0aab5c",
            "5237f14336d9488ab771d58726899fb0",
            "6b3ee8f571cb49078bbfec23cd97952e",
            "b755e280a7ae4d55bdc2d3d250f935df",
            "9ad5630ddcba44a59d1fa1490a3d0dcb",
            "e84d09695fc94496821a4658f8e9db0c",
            "96dbb35dfdf2417db52d1a6caa460d82"
          ]
        },
        "id": "cLbSa3tDVVBB",
        "outputId": "6decb04a-5e43-42d7-e63d-26bb68a961e0"
      },
      "execution_count": 102,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "FileUpload(value={}, accept='.pdf,.docx', description='Upload')"
            ],
            "application/vnd.jupyter.widget-view+json": {
              "version_major": 2,
              "version_minor": 0,
              "model_id": "c1ec52b61f0347fc9bd4a922ad702c21"
            }
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "Text(value='', description='Primary Objective:', placeholder='Enter the primary objective')"
            ],
            "application/vnd.jupyter.widget-view+json": {
              "version_major": 2,
              "version_minor": 0,
              "model_id": "ceaf2d0ed69e48a4950358def791c71a"
            }
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "Text(value='', description='Specific Focus:', placeholder='Enter the specific focus')"
            ],
            "application/vnd.jupyter.widget-view+json": {
              "version_major": 2,
              "version_minor": 0,
              "model_id": "5237f14336d9488ab771d58726899fb0"
            }
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "Button(description='Save & Extract', style=ButtonStyle())"
            ],
            "application/vnd.jupyter.widget-view+json": {
              "version_major": 2,
              "version_minor": 0,
              "model_id": "9ad5630ddcba44a59d1fa1490a3d0dcb"
            }
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "File MICROSOFT EMPLOYMENT AGREEMENT.docx uploaded successfully!\n",
            "\n",
            "--- Results ---\n",
            "Extracted Contract Text (first 500 chars):\n",
            "MICROSOFT EMPLOYMENT AGREEMENT\n",
            "This Employment Agreement (“Agreement”) is made and entered into as of [Start Date], by and between Microsoft Corporation, a corporation organized under the laws of the State of Washington, with its principal office at One Microsoft Way, Redmond, WA 98052-6399 (“Employer”), and Tom Cohen, residing at [Employee Address] (“Employee”).\n",
            "\n",
            "1. Position and Duties\n",
            "1.1 Position: Employee is hereby employed as a [Job Title, e.g., Software Engineer], reporting to [Supervisor’...\n",
            "Primary Objective: Negotiate better terms and ensure compliance\n",
            "Specific Focus: \n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "# Example usage\n",
        "input_state = {\n",
        "    \"contract_text\": contract,\n",
        "    \"primary_objective\": primary_objective,\n",
        "    \"specific_focus\": specific_focus,\n",
        "}\n",
        "# Update the config structure\n",
        "config = {\n",
        "    \"thread_id\": \"1\",  # String instead of nested dict\n",
        "    \"user_id\": \"user_123\"\n",
        "}\n",
        "\n",
        "result = graph.invoke(input_state, config)\n",
        "print(result['final_report'])"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "7TmH5ef1WBOC",
        "outputId": "a043c002-8946-41e6-f0f2-594639314484"
      },
      "execution_count": 103,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "===============================================\n",
            "                  Contract Review Report       \n",
            "===============================================\n",
            "\n",
            "Contract Overview\n",
            "-----------------\n",
            "Primary Objective: Negotiate better terms and ensure compliance\n",
            "Specific Focus: \n",
            "\n",
            "Contract Type: Employment Agreement\n",
            "Industry: Technology\n",
            "\n",
            "Sections and Clauses Analyzed:\n",
            "------------------------------\n",
            "Total Sections Reviewed: 8\n",
            "Total Clauses Analyzed: 8\n",
            "\n",
            "Key Findings and Analysis:\n",
            "--------------------------\n",
            "- ### Clause Analysis\n",
            "The clause in question is the \"Non-Compete and Non-Solicitation\" clause, which is clearly represented in the contract. The key elements of this clause are present, including the duration of the non-compete and non-solicitation obligations (one year following termination), the scope of the non-compete (not engaging in or providing services to a competing business in the same capacity), and the geographical scope (within the United States or any other region where Microsoft operates). The language used is clear and unambiguous, specifying the obligations of the employee during and after employment.\n",
            "\n",
            "No modifications are necessary as the clause is comprehensive and clearly articulated.\n",
            "- ### Clause Analysis\n",
            "The clause in question is the \"Non-Compete and Non-Solicitation\" section of the Microsoft Employment Agreement. Upon review, the clause is clearly represented with the following key elements:\n",
            "\n",
            "1. **Non-Compete Clause (5.1):**\n",
            "   - Specifies the duration of the non-compete obligation (during employment and for one year following termination).\n",
            "   - Defines the scope of the non-compete (not to engage in or provide services to a competing business in the same capacity as their role at Microsoft).\n",
            "   - Specifies the geographical scope (within the United States or any other region where Microsoft operates).\n",
            "\n",
            "2. **Non-Solicitation Clause (5.2):**\n",
            "   - Specifies the duration of the non-solicitation obligation (during employment and for one year following termination).\n",
            "   - Defines the scope of the non-solicitation (not to solicit Microsoft’s clients, customers, or employees for any competitive or conflicting purpose).\n",
            "\n",
            "The language used in both clauses is clear and unambiguous, providing specific details about the obligations and restrictions imposed on the employee. Therefore, no modifications are necessary as the clause is adequately represented and understandable.\n",
            "- ### Clause Analysis\n",
            "The clause in question is the 'Non-Compete and Non-Solicitation' clause, which is clearly represented in the contract. The key elements of this clause are present, including the duration of the non-compete and non-solicitation obligations (one year following termination), the scope of the non-compete (not engaging in or providing services to a competing business in the same capacity as their role at Microsoft), and the geographical scope (within the United States or any other region where Microsoft operates). The language used is clear and unambiguous, making it easy to understand the obligations and restrictions imposed on the employee. No modifications are necessary as the clause is comprehensive and clearly articulated.\n",
            "- ### Clause Analysis\n",
            "The clause in question is the \"Non-Compete and Non-Solicitation\" section of the Microsoft Employment Agreement. Upon review, the clause is clearly represented and includes the following key elements:\n",
            "\n",
            "1. **Non-Compete Clause (5.1):**\n",
            "   - Specifies the duration of the non-compete obligation (during employment and for one year following termination).\n",
            "   - Defines the scope of the non-compete (not to engage in or provide services to a competing business in the same capacity as their role at Microsoft).\n",
            "   - Identifies the geographical scope (within the United States or any other region where Microsoft operates).\n",
            "\n",
            "2. **Non-Solicitation Clause (5.2):**\n",
            "   - Specifies the duration of the non-solicitation obligation (during employment and for one year following termination).\n",
            "   - Clearly states the prohibition against soliciting Microsoft’s clients, customers, or employees for competitive or conflicting purposes.\n",
            "\n",
            "The language used in both clauses is clear and unambiguous, making it easy for the employee to understand their obligations under the agreement. No modifications are necessary as the clause is comprehensive and well-defined.\n",
            "- ### Clause Analysis\n",
            "The clause in question is the \"Non-Compete and Non-Solicitation\" clause, which is clearly represented in the contract. Here is the analysis based on the guidelines:\n",
            "\n",
            "1. **Key Elements Present:**\n",
            "   - The clause includes a non-compete provision (5.1) that restricts the employee from engaging in or providing services to a competing business for one year following termination.\n",
            "   - The clause includes a non-solicitation provision (5.2) that restricts the employee from soliciting Microsoft’s clients, customers, or employees for one year following termination.\n",
            "   - Both provisions specify the duration (one year) and the scope (within the United States or any other region where Microsoft operates).\n",
            "\n",
            "2. **Language Clarity:**\n",
            "   - The language used in the clause is clear and unambiguous. It specifies the actions that are restricted (engaging in competing business and soliciting clients/customers/employees) and the time frame for these restrictions.\n",
            "   - The clause is straightforward and does not contain any complex legal jargon that could confuse the reader.\n",
            "\n",
            "3. **Suggestions for Modifications:**\n",
            "   - No modifications are necessary as the clause is clear and contains all the necessary elements to be enforceable and understandable.\n",
            "- ### Clause Analysis\n",
            "The clause in question is the \"Non-Compete and Non-Solicitation\" section of the Microsoft Employment Agreement. Upon review, the clause is clearly represented with the following key elements:\n",
            "\n",
            "1. **Non-Compete Clause (5.1):**\n",
            "   - **Duration:** The clause specifies that the non-compete obligation lasts during employment and for one year following termination.\n",
            "   - **Scope:** It restricts the employee from engaging in or providing services to a competing business in the same capacity as their role at Microsoft.\n",
            "   - **Geographical Limit:** The restriction applies within the United States or any other region where Microsoft operates.\n",
            "\n",
            "2. **Non-Solicitation Clause (5.2):**\n",
            "   - **Duration:** The non-solicitation obligation also lasts during employment and for one year following termination.\n",
            "   - **Scope:** It prohibits the employee from soliciting Microsoft’s clients, customers, or employees for any competitive or conflicting purpose.\n",
            "\n",
            "The language used in both clauses is clear and unambiguous, providing specific details about the duration, scope, and geographical limits of the restrictions. Therefore, no modifications are necessary as the clause is adequately represented and understandable.\n",
            "- ### Clause Analysis\n",
            "The clause in question is the \"Non-Compete and Non-Solicitation\" section, which is clearly represented in the contract. The key elements of this clause are present, including the duration of the non-compete and non-solicitation obligations (one year following termination), the scope of the non-compete (not engaging in or providing services to a competing business in the same capacity), and the geographical scope (within the United States or any other region where Microsoft operates). The language used is clear and unambiguous, specifying the obligations of the employee during and after employment.\n",
            "\n",
            "No modifications are necessary as the clause is comprehensive and clearly articulated.\n",
            "- ### Clause Analysis\n",
            "The clause in question is the \"Non-Compete and Non-Solicitation\" section, which is clearly represented in the contract. The key elements of this clause are present, including the duration of the non-compete and non-solicitation obligations (one year following termination), the scope of the non-compete (not engaging in or providing services to a competing business in the same capacity), and the geographical scope (within the United States or any other region where Microsoft operates). The language used is clear and unambiguous, making it easy to understand the obligations and restrictions imposed on the employee.\n",
            "\n",
            "No modifications are necessary as the clause is comprehensive and clearly articulated.\n",
            "\n",
            "Highlights of Suggested Modifications:\n",
            "--------------------------------------\n",
            "The modifications to the contract primarily focus on ensuring legal compliance, clarity, and enforceability. Here's a summary from a legal expert's perspective:\n",
            "\n",
            "1. **Non-Compete Clauses**: Several modifications address the enforceability of non-compete clauses, which can vary by jurisdiction. Suggestions include reducing the duration from one year to six months, narrowing the geographic scope, and adding clauses that acknowledge the limitations imposed by applicable law. These changes aim to make the clauses more reasonable and legally defensible.\n",
            "\n",
            "2. **Termination Clauses**: Modifications to termination clauses include defining \"cause\" for termination to prevent disputes, ensuring compliance with applicable employment laws, and emphasizing fairness in at-will employment. These changes provide clarity and protect both parties from potential legal challenges.\n",
            "\n",
            "3. **Confidential Information**: The modifications clarify the types of confidential information and extend the confidentiality obligation beyond employment. This strengthens the protection of sensitive information and ensures clarity on the employee's obligations.\n",
            "\n",
            "4. **Severance Benefits**: Changes to severance clauses include providing certainty about eligibility and requiring a release of claims agreement. These modifications aim to protect the employer from future legal claims while providing transparency to the employee.\n",
            "\n",
            "5. **Intellectual Property**: Clarifications are made regarding what constitutes intellectual property related to Microsoft's business, including a requirement for prompt disclosure. This helps prevent ambiguity and protects Microsoft's interests.\n",
            "\n",
            "6. **Base Salary**: Modifications include clauses about annual review and potential adjustments based on performance and market conditions, as well as standard withholdings and deductions. These changes ensure transparency and compliance with tax laws.\n",
            "\n",
            "7. **Non-Solicitation Clauses**: Similar to non-compete clauses, the duration of non-solicitation obligations is reduced to six months to make them more balanced and legally defensible.\n",
            "\n",
            "Overall, the modifications aim to align the contract with current legal standards, reduce the risk of disputes, and ensure that the terms are clear and enforceable for both parties.\n",
            "\n",
            "Compliance and Risk Assessment:\n",
            "-------------------------------\n",
            "- The contract has been reviewed for compliance with relevant laws and regulations.\n",
            "- Potential risks and mitigation strategies have been identified.\n",
            "- Tailored suggestions have been provided to enhance the contract’s effectiveness.\n",
            "\n",
            "Final Notes:\n",
            "------------\n",
            "Please ensure all suggested modifications are incorporated and reviewed by a legal expert before finalizing the contract.\n",
            "\n",
            "===============================================\n",
            "                  End of Report               \n",
            "===============================================\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## Download modified contract (optional)\n",
        "\n",
        "### You can run this cell to download the contract with the suggested modifications."
      ],
      "metadata": {
        "id": "PfhniIv5YnYc"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from docx import Document\n",
        "from docx.shared import RGBColor\n",
        "from docx.oxml import OxmlElement\n",
        "\n",
        "def apply_modifications_to_contract(contract_text, all_modifications, output_path=\"Modified_Contract.docx\"):\n",
        "    \"\"\"\n",
        "    Applies modifications to the given contract text and outputs a modified DOCX file.\n",
        "\n",
        "    Parameters:\n",
        "    - contract_text (str): The original contract text.\n",
        "    - all_modifications (list): List of modifications, where each modification is a dictionary with:\n",
        "        - \"original_text\" (str): The text to be replaced.\n",
        "        - \"suggested_text\" (str): The suggested replacement text.\n",
        "        - \"reason\" (str): The reason for the modification.\n",
        "    - output_path (str): The output file path for the modified contract (default: \"Modified_Contract.docx\").\n",
        "    \"\"\"\n",
        "    # Create a new document\n",
        "    doc = Document()\n",
        "\n",
        "    # Split the contract text into paragraphs\n",
        "    paragraphs = contract_text.split(\"\\n\")\n",
        "\n",
        "    def add_strikethrough(run):\n",
        "          r = run._element\n",
        "          rPr = r.get_or_add_rPr()\n",
        "          strike = OxmlElement(\"w:strike\")\n",
        "          rPr.append(strike)\n",
        "\n",
        "    # Process each paragraph\n",
        "    for paragraph_text in paragraphs:\n",
        "        # Check if the paragraph contains any modifications\n",
        "        modified = False\n",
        "        for mod in all_modifications:\n",
        "            if mod.original_text in paragraph_text:\n",
        "                modified = True\n",
        "\n",
        "                # Create a new paragraph with the modifications\n",
        "                p = doc.add_paragraph()\n",
        "\n",
        "                # Add original text with strikethrough\n",
        "                original_run = p.add_run(mod.original_text)\n",
        "                add_strikethrough(original_run)\n",
        "\n",
        "                # Add suggested text in red\n",
        "                suggested_run = p.add_run(\"\\n\" + mod.suggested_text)\n",
        "                suggested_run.font.color.rgb = RGBColor(255, 0, 0)  # Red\n",
        "\n",
        "                # Add reason in gray\n",
        "                reason_run = p.add_run(\"\\n(\" + mod.reason + \")\")\n",
        "                reason_run.font.color.rgb = RGBColor(128, 128, 128)  # Gray\n",
        "\n",
        "                break\n",
        "\n",
        "        # If no modifications, add the paragraph as-is\n",
        "        if not modified:\n",
        "            doc.add_paragraph(paragraph_text)\n",
        "\n",
        "    # Save the modified document\n",
        "    doc.save(output_path)\n",
        "    print(f\"Modified contract saved to {output_path}\")\n",
        "\n",
        "\n",
        "\n",
        "contract_text = result[\"contract_text\"]\n",
        "clause_modifications = result[\"clause_modifications\"]\n",
        "planner_modifications = result[\"modifications\"]\n",
        "\n",
        "all_modifications = clause_modifications + planner_modifications\n",
        "\n",
        "apply_modifications_to_contract(contract_text, all_modifications, \"Modified_Contract.docx\")\n"
      ],
      "metadata": {
        "id": "lfvAZya1Yyrh",
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "outputId": "d65ca7d1-105c-4e9f-d7c8-77d0b85442c7"
      },
      "execution_count": 104,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Modified contract saved to Modified_Contract.docx\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [],
      "metadata": {
        "id": "FwKzd2XSZfF9"
      },
      "execution_count": null,
      "outputs": []
    }
  ],
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3 (ipykernel)",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.10.0"
    },
    "widgets": {
      "application/vnd.jupyter.widget-state+json": {
        "c1ec52b61f0347fc9bd4a922ad702c21": {
          "model_module": "@jupyter-widgets/controls",
          "model_name": "FileUploadModel",
          "model_module_version": "1.5.0",
          "state": {
            "_counter": 1,
            "_dom_classes": [],
            "_model_module": "@jupyter-widgets/controls",
            "_model_module_version": "1.5.0",
            "_model_name": "FileUploadModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/controls",
            "_view_module_version": "1.5.0",
            "_view_name": "FileUploadView",
            "accept": ".pdf,.docx",
            "button_style": "",
            "data": [
              null
            ],
            "description": "Upload",
            "description_tooltip": null,
            "disabled": false,
            "error": "",
            "icon": "upload",
            "layout": "IPY_MODEL_4b19a34d51484f3cbaacb77b2d04e210",
            "metadata": [
              {
                "name": "MICROSOFT EMPLOYMENT AGREEMENT.docx",
                "type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                "size": 15441,
                "lastModified": 1732449284000
              }
            ],
            "multiple": false,
            "style": "IPY_MODEL_d89a6ec0ed8546079a964b74fd58ff59"
          }
        },
        "4b19a34d51484f3cbaacb77b2d04e210": {
          "model_module": "@jupyter-widgets/base",
          "model_name": "LayoutModel",
          "model_module_version": "1.2.0",
          "state": {
            "_model_module": "@jupyter-widgets/base",
            "_model_module_version": "1.2.0",
            "_model_name": "LayoutModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/base",
            "_view_module_version": "1.2.0",
            "_view_name": "LayoutView",
            "align_content": null,
            "align_items": null,
            "align_self": null,
            "border": null,
            "bottom": null,
            "display": null,
            "flex": null,
            "flex_flow": null,
            "grid_area": null,
            "grid_auto_columns": null,
            "grid_auto_flow": null,
            "grid_auto_rows": null,
            "grid_column": null,
            "grid_gap": null,
            "grid_row": null,
            "grid_template_areas": null,
            "grid_template_columns": null,
            "grid_template_rows": null,
            "height": null,
            "justify_content": null,
            "justify_items": null,
            "left": null,
            "margin": null,
            "max_height": null,
            "max_width": null,
            "min_height": null,
            "min_width": null,
            "object_fit": null,
            "object_position": null,
            "order": null,
            "overflow": null,
            "overflow_x": null,
            "overflow_y": null,
            "padding": null,
            "right": null,
            "top": null,
            "visibility": null,
            "width": null
          }
        },
        "d89a6ec0ed8546079a964b74fd58ff59": {
          "model_module": "@jupyter-widgets/controls",
          "model_name": "ButtonStyleModel",
          "model_module_version": "1.5.0",
          "state": {
            "_model_module": "@jupyter-widgets/controls",
            "_model_module_version": "1.5.0",
            "_model_name": "ButtonStyleModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/base",
            "_view_module_version": "1.2.0",
            "_view_name": "StyleView",
            "button_color": null,
            "font_weight": ""
          }
        },
        "ceaf2d0ed69e48a4950358def791c71a": {
          "model_module": "@jupyter-widgets/controls",
          "model_name": "TextModel",
          "model_module_version": "1.5.0",
          "state": {
            "_dom_classes": [],
            "_model_module": "@jupyter-widgets/controls",
            "_model_module_version": "1.5.0",
            "_model_name": "TextModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/controls",
            "_view_module_version": "1.5.0",
            "_view_name": "TextView",
            "continuous_update": true,
            "description": "Primary Objective:",
            "description_tooltip": null,
            "disabled": false,
            "layout": "IPY_MODEL_b5d85e4e6cb04eb59d71861d5e4dfe83",
            "placeholder": "Enter the primary objective",
            "style": "IPY_MODEL_0ae65eea03cb40bdae641fe9cd0aab5c",
            "value": "Negotiate better terms and ensure compliance"
          }
        },
        "b5d85e4e6cb04eb59d71861d5e4dfe83": {
          "model_module": "@jupyter-widgets/base",
          "model_name": "LayoutModel",
          "model_module_version": "1.2.0",
          "state": {
            "_model_module": "@jupyter-widgets/base",
            "_model_module_version": "1.2.0",
            "_model_name": "LayoutModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/base",
            "_view_module_version": "1.2.0",
            "_view_name": "LayoutView",
            "align_content": null,
            "align_items": null,
            "align_self": null,
            "border": null,
            "bottom": null,
            "display": null,
            "flex": null,
            "flex_flow": null,
            "grid_area": null,
            "grid_auto_columns": null,
            "grid_auto_flow": null,
            "grid_auto_rows": null,
            "grid_column": null,
            "grid_gap": null,
            "grid_row": null,
            "grid_template_areas": null,
            "grid_template_columns": null,
            "grid_template_rows": null,
            "height": null,
            "justify_content": null,
            "justify_items": null,
            "left": null,
            "margin": null,
            "max_height": null,
            "max_width": null,
            "min_height": null,
            "min_width": null,
            "object_fit": null,
            "object_position": null,
            "order": null,
            "overflow": null,
            "overflow_x": null,
            "overflow_y": null,
            "padding": null,
            "right": null,
            "top": null,
            "visibility": null,
            "width": null
          }
        },
        "0ae65eea03cb40bdae641fe9cd0aab5c": {
          "model_module": "@jupyter-widgets/controls",
          "model_name": "DescriptionStyleModel",
          "model_module_version": "1.5.0",
          "state": {
            "_model_module": "@jupyter-widgets/controls",
            "_model_module_version": "1.5.0",
            "_model_name": "DescriptionStyleModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/base",
            "_view_module_version": "1.2.0",
            "_view_name": "StyleView",
            "description_width": ""
          }
        },
        "5237f14336d9488ab771d58726899fb0": {
          "model_module": "@jupyter-widgets/controls",
          "model_name": "TextModel",
          "model_module_version": "1.5.0",
          "state": {
            "_dom_classes": [],
            "_model_module": "@jupyter-widgets/controls",
            "_model_module_version": "1.5.0",
            "_model_name": "TextModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/controls",
            "_view_module_version": "1.5.0",
            "_view_name": "TextView",
            "continuous_update": true,
            "description": "Specific Focus:",
            "description_tooltip": null,
            "disabled": false,
            "layout": "IPY_MODEL_6b3ee8f571cb49078bbfec23cd97952e",
            "placeholder": "Enter the specific focus",
            "style": "IPY_MODEL_b755e280a7ae4d55bdc2d3d250f935df",
            "value": ""
          }
        },
        "6b3ee8f571cb49078bbfec23cd97952e": {
          "model_module": "@jupyter-widgets/base",
          "model_name": "LayoutModel",
          "model_module_version": "1.2.0",
          "state": {
            "_model_module": "@jupyter-widgets/base",
            "_model_module_version": "1.2.0",
            "_model_name": "LayoutModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/base",
            "_view_module_version": "1.2.0",
            "_view_name": "LayoutView",
            "align_content": null,
            "align_items": null,
            "align_self": null,
            "border": null,
            "bottom": null,
            "display": null,
            "flex": null,
            "flex_flow": null,
            "grid_area": null,
            "grid_auto_columns": null,
            "grid_auto_flow": null,
            "grid_auto_rows": null,
            "grid_column": null,
            "grid_gap": null,
            "grid_row": null,
            "grid_template_areas": null,
            "grid_template_columns": null,
            "grid_template_rows": null,
            "height": null,
            "justify_content": null,
            "justify_items": null,
            "left": null,
            "margin": null,
            "max_height": null,
            "max_width": null,
            "min_height": null,
            "min_width": null,
            "object_fit": null,
            "object_position": null,
            "order": null,
            "overflow": null,
            "overflow_x": null,
            "overflow_y": null,
            "padding": null,
            "right": null,
            "top": null,
            "visibility": null,
            "width": null
          }
        },
        "b755e280a7ae4d55bdc2d3d250f935df": {
          "model_module": "@jupyter-widgets/controls",
          "model_name": "DescriptionStyleModel",
          "model_module_version": "1.5.0",
          "state": {
            "_model_module": "@jupyter-widgets/controls",
            "_model_module_version": "1.5.0",
            "_model_name": "DescriptionStyleModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/base",
            "_view_module_version": "1.2.0",
            "_view_name": "StyleView",
            "description_width": ""
          }
        },
        "9ad5630ddcba44a59d1fa1490a3d0dcb": {
          "model_module": "@jupyter-widgets/controls",
          "model_name": "ButtonModel",
          "model_module_version": "1.5.0",
          "state": {
            "_dom_classes": [],
            "_model_module": "@jupyter-widgets/controls",
            "_model_module_version": "1.5.0",
            "_model_name": "ButtonModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/controls",
            "_view_module_version": "1.5.0",
            "_view_name": "ButtonView",
            "button_style": "",
            "description": "Save & Extract",
            "disabled": false,
            "icon": "",
            "layout": "IPY_MODEL_e84d09695fc94496821a4658f8e9db0c",
            "style": "IPY_MODEL_96dbb35dfdf2417db52d1a6caa460d82",
            "tooltip": ""
          }
        },
        "e84d09695fc94496821a4658f8e9db0c": {
          "model_module": "@jupyter-widgets/base",
          "model_name": "LayoutModel",
          "model_module_version": "1.2.0",
          "state": {
            "_model_module": "@jupyter-widgets/base",
            "_model_module_version": "1.2.0",
            "_model_name": "LayoutModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/base",
            "_view_module_version": "1.2.0",
            "_view_name": "LayoutView",
            "align_content": null,
            "align_items": null,
            "align_self": null,
            "border": null,
            "bottom": null,
            "display": null,
            "flex": null,
            "flex_flow": null,
            "grid_area": null,
            "grid_auto_columns": null,
            "grid_auto_flow": null,
            "grid_auto_rows": null,
            "grid_column": null,
            "grid_gap": null,
            "grid_row": null,
            "grid_template_areas": null,
            "grid_template_columns": null,
            "grid_template_rows": null,
            "height": null,
            "justify_content": null,
            "justify_items": null,
            "left": null,
            "margin": null,
            "max_height": null,
            "max_width": null,
            "min_height": null,
            "min_width": null,
            "object_fit": null,
            "object_position": null,
            "order": null,
            "overflow": null,
            "overflow_x": null,
            "overflow_y": null,
            "padding": null,
            "right": null,
            "top": null,
            "visibility": null,
            "width": null
          }
        },
        "96dbb35dfdf2417db52d1a6caa460d82": {
          "model_module": "@jupyter-widgets/controls",
          "model_name": "ButtonStyleModel",
          "model_module_version": "1.5.0",
          "state": {
            "_model_module": "@jupyter-widgets/controls",
            "_model_module_version": "1.5.0",
            "_model_name": "ButtonStyleModel",
            "_view_count": null,
            "_view_module": "@jupyter-widgets/base",
            "_view_module_version": "1.2.0",
            "_view_name": "StyleView",
            "button_color": null,
            "font_weight": ""
          }
        }
      }
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}